package tech.fangsong.client.manager;

import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.concurrent.DefaultThreadFactory;
import io.netty.util.concurrent.Future;
import tech.fangsong.client.ack.MsgAckManager;
import tech.fangsong.client.bootstrap.ImClient;
import tech.fangsong.client.service.SequenceService;
import tech.fangsong.im.core.exception.ImException;
import tech.fangsong.im.core.protocol.ImMsg;
import tech.fangsong.client.ack.MsgAckTimerTask;

import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

public class ImClientManager {
    private ImClient client;
    private int sendRetryDuration;
    private int sendRetry;
    private NioSocketChannel channel;
    private int connectRetry;
    private String host;
    private int port;
    private NioEventLoopGroup ackGroup;

    private final HashedWheelTimer ackTimer = new HashedWheelTimer(new DefaultThreadFactory("send-msg-ack-retry-duration"), 100,
            TimeUnit.MILLISECONDS, 1024, false);

    private ImClientManager(){

    }

    /**
     * 发送消息
     *
     * @param msg 消息体
     */
    public void send(ImMsg msg) {
        if (channel == null) {
            reconnect();
        }
        if (!channel.isActive()) {
            channel.close().addListener(future -> {
                if (future.isSuccess()) {
                    reconnect();
                }
            });
        }else {
            // 发送消息
            long sequenceId = SequenceService.getInstance().generateSequenceId();
            msg.setSequence(sequenceId);
            if (!channel.isWritable()) {
                try {
                    channel.writeAndFlush(msg).sync().addListener(future -> {
                        if(future.isSuccess()){
                            operationSuccess(sequenceId, msg);
                        }
                    });
                } catch (InterruptedException e) {

                }
            }else {
                channel.writeAndFlush(msg).addListener(future -> {
                    if (future.isSuccess()) {
                        operationSuccess(sequenceId, msg);
                    }
                });
            }
        }
    }

    private void operationSuccess(long sequenceId, ImMsg msg) {
        Timeout timeout = ackTimer.newTimeout(new MsgAckTimerTask(channel, sendRetryDuration, sendRetry, msg), sendRetryDuration, TimeUnit.MILLISECONDS);
        MsgAckManager.ACK_MSG_TIMEOUT_LIST.put(sequenceId, timeout);
    }

    public void connectFuture(Future<?> future){
        if(future.isSuccess()){
           return;
        }
        if(connectRetry == 0){
            throw new ImException("与服务器建立连接失败");
        }
        /*
         * 当网络异常恢复后，大量客户端可能会同时发起TCP重连及进行应用层请求，可能会造成服务端过载、网络带宽耗尽等问题，所以增加随机退让机制
         */
        int delay = 1 + ThreadLocalRandom.current().nextInt(0, 5);
        ackGroup.schedule(() -> {
            connect(host, port);
            connectRetry--;
        }, delay, TimeUnit.SECONDS);
    }


    /**
     * connect
     * @param host host
     * @param port port
     * @return res
     */
    public boolean connect(String host, Integer port) {
        channel = client.connect(host, port, this::connectFuture);
        this.host = host;
        this.port = port;
        return true;
    }

    public void disconnect(){
        ackTimer.stop();
        client.destroy();
    }

    /**
     * reconnect
     */
    public void reconnect() {
        channel = client.connect(host, port, this::connectFuture);
    }


    public static class Builder {
        private int sendRetryDuration;
        private int sendRetry;
        private int connectRetry;

        public Builder() {
        }

        public void setSendRetryDuration(int sendRetryDuration) {
            this.sendRetryDuration = sendRetryDuration;
        }

        public Builder setSendRetry(int sendRetry) {
            this.sendRetry = sendRetry;
            return this;
        }


        public Builder setConnectRetry(int connectRetry) {
            this.connectRetry = connectRetry;
            return this;
        }

        public ImClientManager build() {
            ImClientManager clientManager = new ImClientManager();
            clientManager.sendRetryDuration = sendRetryDuration;
            clientManager.sendRetry = sendRetry;
            clientManager.connectRetry = connectRetry;
            clientManager.client = new ImClient();
            clientManager.ackGroup = new NioEventLoopGroup(1);
            return clientManager;
        }
    }


}
